package com.yunkss.eftool.filter;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.yunkss.eftool.crypto.EftoolEncryptUtils;
import com.yunkss.eftool.crypto.SM2Util;
import io.netty.buffer.ByteBufAllocator;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 完整性校验以及解密 拦截器
 *
 * @author csx
 * @date 2022/2/9 11:28
 */
@Slf4j
@Component
public class ValidateDecryptFilter implements Ordered, GlobalFilter {

    @Autowired
    private EftoolEncryptUtils encryptUtils;
    private Random random = new Random();

    //存储前端加密的请求参数
    private final ConcurrentHashMap<String, String> requestParamMap = new ConcurrentHashMap<>();
    //存储前端加密的sm4key
    private final ConcurrentHashMap<String, String> sm4KeyMap = new ConcurrentHashMap<>();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //如果是get直接放行
        if (Objects.equals(exchange.getRequest().getMethod(), HttpMethod.GET)) {
            return chain.filter(exchange);
        }
        // 根据header中参数判断是否需要加密
        ServerHttpRequest request = exchange.getRequest();
        String reqType = request.getHeaders().getFirst("Req-Type");
        if ("uploadOrDownload".equals(reqType)) {
            return chain.filter(exchange);
        }
        if (true == false) {
            //生成本次随机前缀
            String prefix = String.valueOf(random.nextInt(10000) + DateUtil.current());
            return DataBufferUtils.join(request.getBody()).flatMap(dataBuffer -> {
                //请求内容中的字符串
                String bodyStr = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()).toString();
                SignValidate signValidate = null;
                try {
                    signValidate = new SignValidate(request).invoke(prefix, bodyStr);
                } catch (NoSuchPaddingException | InvalidAlgorithmParameterException | NoSuchAlgorithmException |
                         IllegalBlockSizeException | BadPaddingException | NoSuchProviderException |
                         InvalidKeyException e) {
                    e.printStackTrace();
                }
                if (signValidate.is()) {
                    ThreadUtil.execute(() -> {
                        //完整性审计(优化全局用户信息，用于审计)
                    });
                    return chain.filter(exchange);
                }
                ;
                //解析后的请求参数
                String decReqParam = signValidate.getDecReqParam();
                return chain.filter(exchange.mutate().request(requestDecorator(exchange, decReqParam)).response(responseDecorator(exchange, prefix)).build());
            });
        }
        return chain.filter(exchange);
    }


    /**
     * 请求
     *
     * @param exchange
     * @param decReqParam
     * @return
     */
    private ServerHttpRequest requestDecorator(ServerWebExchange exchange, String decReqParam) {
        URI uri = exchange.getRequest().getURI();
        URI newUri = UriComponentsBuilder.fromUri(uri).build(true).toUri();
        ServerHttpRequest requestNew = exchange.getRequest().mutate().uri(newUri).build();
        DataBuffer bodyDataBuffer = stringBuffer(decReqParam);
        Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
        // 定义新的消息头
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        // 由于修改了传递参数，需要重新设置CONTENT_LENGTH，长度是字节长度，不是字符串长度
        int length = decReqParam.getBytes().length;
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        headers.setContentLength(length);
        String contentType = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
        // 设置CONTENT_TYPE
        if (StrUtil.isNotBlank(contentType)) {
            headers.set(HttpHeaders.CONTENT_TYPE, contentType);
        }
        // 由于post的body只能订阅一次，由于上面代码中已经订阅过一次body。所以要再次封装请求到request才行，不然会报错请求已经订阅过
        requestNew = new ServerHttpRequestDecorator(requestNew) {
            @Override
            public HttpHeaders getHeaders() {
                long contentLength = headers.getContentLength();
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                if (contentLength > 0) {
                    httpHeaders.setContentLength(contentLength);
                } else {
                    httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return httpHeaders;
            }

            @Override
            public Flux<DataBuffer> getBody() {
                return bodyFlux;
            }
        };
        // 封装request，传给下一级
        requestNew.mutate().header(HttpHeaders.CONTENT_LENGTH, Integer.toString(decReqParam.length()));
        return requestNew;
    }

    /**
     * 响应加密
     *
     * @param exchange
     * @param prefix
     * @return
     */
    private ServerHttpResponseDecorator responseDecorator(ServerWebExchange exchange, String prefix) {
        ServerHttpResponse response = exchange.getResponse();
        // 请求地址
        String path = exchange.getRequest().getURI().getPath();
        DataBufferFactory bufferFactory = response.bufferFactory();
        return new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                AtomicReference<String> responseStr = new AtomicReference<>("");
                return super.writeWith(DataBufferUtils.join(body).doOnNext(dBuffer -> {
                    //响应字符串
                    String resStr = dBuffer.toString(StandardCharsets.UTF_8);
                    String sm4Key = "";
                    try {
                        //将数据转换
                        byte[] respByte = HexUtil.decodeHex(sm4KeyMap.get(path + prefix));

                        byte[] c1C3C2 = SM2Util.changeAsn1ToC1C3C2(respByte);

                        String str = HexUtil.encodeHexStr(c1C3C2);
                        //需要将前端给的key解密
                        sm4Key = encryptUtils.getSM2().decrypt(str, "7713d336bcbbffb8b7f9cab8db984a5c989a0b07697f569a06d5cd38e1351d07");
                        //移除
                        sm4KeyMap.remove(path + prefix);
                    } catch (InvalidCipherTextException e) {
                        e.printStackTrace();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                    //组装给前端返回数据
                    JSONObject json = new JSONObject();
                    json.put("ef_request_params", requestParamMap.get(path + prefix));
                    try {
                        String key = sm4Key.substring(32);
                        String iv = sm4Key.substring(0, 32);
                        json.put("data", encryptUtils.getSM4().encryptCBC(key, iv, resStr.trim()).trim());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    responseStr.set(json.toJSONString());
                    requestParamMap.remove(path + prefix);
                    //不设置长度可能会返回不全数据
                    response.getHeaders().set("Content-Length", String.valueOf(responseStr.get().getBytes().length));
                }).map(dbf -> {
                    try {
                        byte[] uppedContent = new String(responseStr.get().getBytes(), Charset.forName("UTF-8")).getBytes();
                        return bufferFactory.wrap(uppedContent);
                    } finally {
                        //防止内存泄漏
                        DataBufferUtils.release(dbf);
                    }
                }));
            }
        };
    }

    private DataBuffer stringBuffer(String value) {
        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
        NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
        DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
        buffer.write(bytes);
        return buffer;
    }

    @Override
    public int getOrder() {
        return -120;
    }

    private class SignValidate {
        private boolean myResult;
        private ServerHttpRequest request;
        private String decReqParam;

        public SignValidate(ServerHttpRequest request) {
            this.request = request;
        }

        boolean is() {
            return myResult;
        }

        public String getDecReqParam() {
            return decReqParam;
        }

        /**
         * 验签
         *
         * @return
         */
        public SignValidate invoke(String prefix, String dataBuffer) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, NoSuchProviderException, InvalidKeyException {
            HttpMethod method = request.getMethod();
            HttpHeaders headers = request.getHeaders();
            //获取请求头中加密的sm4 key
            String efRandomKey = headers.getFirst("ef_random_key");
            //获取加密的请求参数(此处解密需要判断请求方法)
            String requestStr = "";
            if (method == HttpMethod.POST || method == HttpMethod.PUT) {
                //从请求里获取Post请求体
                requestStr = dataBuffer;
            } else if (method == HttpMethod.GET || method == HttpMethod.DELETE) {
                MultiValueMap<String, String> requestQueryParams = request.getQueryParams();
                requestStr = requestQueryParamsToString(requestQueryParams);
            }
            if (!StringUtils.isEmpty(requestStr)) {
                //将请求字符串转换成json对象
                JSONObject json = JSON.parseObject(requestStr);
                //加密的请求参数字符串
                String requestParams = json.getString("ef_request_params");
                //合法性校验加密的字符串参数
                String fingerprint = json.getString("ef_fingerprint");
                //将请求header中的sm4 key使用sm2 解密
                String sm4Key = "";
                try {
                    //将数据转换
                    byte[] reqParams = HexUtil.decodeHex(efRandomKey);
                    //将前端使用ASN.1加密的数据转换
                    byte[] c1C3C2 = SM2Util.changeAsn1ToC1C3C2(reqParams);
                    //C1C3C2格式的加密字符串
                    String str = HexUtil.encodeHexStr(c1C3C2);
                    //得到SM4的密钥和iv
                    sm4Key = encryptUtils.getSM2().decrypt(str, "7713d336bcbbffb8b7f9cab8db984a5c989a0b07697f569a06d5cd38e1351d07");
                } catch (Exception e) {
                    myResult = true;
                    return this;
                }
                String path = request.getURI().getPath();
                sm4KeyMap.put(path + prefix, efRandomKey);
                if (!StringUtils.isEmpty(requestParams)) {
                    requestParamMap.put(path + prefix, requestParams);
                }
                String key = sm4Key.substring(32);
                String iv = sm4Key.substring(0, 32);
                try {
                    decReqParam = encryptUtils.getSM4().decryptCBC(key, iv, requestParams).trim();
                } catch (Exception e) {
                    myResult = true;
                    return this;
                }
                String finalStr = encryptUtils.getSM3().digest(decReqParam);
                //将sm4的数据解密并跟sm3 hash后的数据比较，相同则完整性一致，否则被篡改
                if (!fingerprint.equals(finalStr)) {
                    ThreadUtil.execute(() -> {
                        //完整性校验不通过
                    });
                    myResult = true;
                    return this;
                }
            }
            myResult = false;
            return this;
        }


    }

    /**
     * 将Map参数拼接位字符串
     *
     * @param requestQueryParams
     * @return
     */
    private String requestQueryParamsToString(MultiValueMap<String, String> requestQueryParams) {
        List<String> paramsArray = new ArrayList<>();
        for (String key : requestQueryParams.keySet()) {
            String temp = key + "=" + requestQueryParams.get(key).get(0);
            paramsArray.add(temp);
        }
        String params = "";
        for (int i = 0; i < paramsArray.size(); i++) {
            if (i == paramsArray.size() - 1) {
                params = params + paramsArray.get(i);
            } else {
                params = params + paramsArray.get(i) + "&";
            }
        }
        return params;
    }

}
